﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;
using System.Threading;

namespace Chat.Client.GUI
{
    /// <summary>
    /// The chat client GUI.
    /// </summary>
    public partial class ClientMainForm : Form
    {
        /// <summary>
        /// The client end point used to communicate with the chat server.
        /// </summary>
        private ClientEndPoint clientEndPoint;
        /// <summary>
        /// Dialog for chat server credentials and user login info.
        /// </summary>
        private ConnectToDialog connectToDlg;

        public ClientMainForm()
        {
            InitializeComponent();

            SetSelectedUserChangedEvent(true);
        }

        /// <summary>
        /// Performs all the actions necessary to connect to the chat server:
        /// 1. Asks user for server credentials and user login info
        /// by running the ConnectToDialog.
        /// 2. Creates the instanse of ClientEndPoint. Behind the scene,
        /// The ClientEndPoint ctor, runs the login procedure agains the server.
        /// 3. Starts the ClientEndPoint command receiver background thread,
        /// who is responsible for receiving server commands.
        /// 4. Updates the main form layout to 'connected'.
        /// </summary>
        protected virtual void Connect()
        {
            if (clientEndPoint != null)
            {
                MessageBox.Show("Already connected");
                return;
            }

            DialogResult dr = connectToDlg.ShowDialog();

            if (dr != DialogResult.OK)
                return;

            CreateEndPoint();

            if (clientEndPoint == null)
                return;

            try
            {
                ClientEndPoint.Start(clientEndPoint);
            }
            catch (Exception ex)
            {
                //
                // No need to dispose the clientEndPoint instanse. The static wrapper
                // ClientEndPoint.Start will take care of that upon exception. 
                clientEndPoint = null;

                MessageBox.Show(string.Format("Oops, failed to start: {0}", ex.Message));
                return;
            }

            UpdateConnectionStateLayout(true);
        }
        /// <summary>
        /// Performs all the actions necessary to disconnect from the chat server:
        /// 1. Disposes the clientEndPoint instanse.
        /// 2. Updates the main form layout to 'disconnected'.
        /// </summary>
        protected virtual void Disconnect()
        {
            if (clientEndPoint == null)
            {
                MessageBox.Show("Not connected");
                return;
            }

            try
            {
                clientEndPoint.Dispose();
            }
            catch (Exception ex)
            {
                MessageBox.Show("Failed to disconnect:\n" + ex.Message);
                return;
            }

            clientEndPoint = null;
            UpdateConnectionStateLayout(false);
        }


        /// <summary>
        /// Delegate for invocation the methods, that perform control updates upon
        /// the client state change from disconnected to connected or vice versa.
        /// </summary>
        /// <param name="connected">Whether the client disconnected or connected.</param>
        protected delegate void UpdateConnectionStateDelegate(bool connected);
        /// <summary>
        /// Updates ToollStripStatusLabel of the form, which idicates the client
        /// state (disconnected or connected).
        /// </summary>
        /// <param name="connected">Whether the client connected or disconnected.</param>
        protected virtual void UpdateConnectionStateLabel(bool connected)
        {
            if (statusStripClient.InvokeRequired)
            {
                statusStripClient.Invoke(
                    new UpdateConnectionStateDelegate(UpdateConnectionStateLabel),
                    new object[] { connected });
                return;
            }

            toolStripStatusLabelConnected.Text = connected ? "Connected" : "Disconnected";
            toolStripStatusLabelConnected.ForeColor = connected ? Color.Blue : Color.Red;    
        }
        /// <summary>
        /// Updates the main menu layout which is different for disconnected and connected
        /// client's states.
        /// </summary>
        /// <param name="connected">Whether the client connected or disconnected.</param>
        protected virtual void UpdateMainMenuLayout(bool connected)
        {
            if (menuStripMain.InvokeRequired)
            {
                menuStripMain.Invoke(
                    new UpdateConnectionStateDelegate(UpdateMainMenuLayout),
                    new object[] { connected });
                return;
            }

            connectToolStripMenuItem.Enabled = !connected;
            disconnectToolStripMenuItem.Enabled = connected;
        }
        /// <summary>
        /// Updates the send buttons layout. Buttons enabled only when the client is
        /// connected to the server.
        /// </summary>
        /// <param name="connected">Whether the client connected or disconnected.</param>
        protected virtual void UpdateSendButtonLayout(bool connected)
        {
            if (btnSend.InvokeRequired)
            {
                btnSend.Invoke(
                    new UpdateConnectionStateDelegate(UpdateSendButtonLayout),
                    new object[] { connected });
                return;
            }

            if (!connected)
            {
                btnSend.Enabled = btnSendPrivate.Enabled = false;
                return;
            }

            //
            // The regular send button will be always enabled in the 'connected' state.
            btnSend.Enabled = true;

            //
            // The private send button will be enabled only when some user selected
            // in the listbox with online users.
            btnSendPrivate.Enabled =
                lbxOnlineUsers.Items.Count > 0 &&
                lbxOnlineUsers.SelectedIndex >= 0;
        }
        /// <summary>
        /// Updates the main form title, which is different for disconnected and connected
        /// client's states.
        /// </summary>
        /// <param name="connected">Whether the client connected or disconnected.</param>
        protected virtual void UpdateMainWindowLayout(bool connected)
        {
            if (this.InvokeRequired)
            {
                this.Invoke(
                    new UpdateConnectionStateDelegate(UpdateMainWindowLayout),
                    new object[] { connected });
                return;
            }

            if (connected)
            {
                this.Text = "Chat, " + clientEndPoint.LoginData.User;
            }
            else
            {
                this.Text = "Chat (disconnected)";
            }
        }

        /// <summary>
        /// Delegate to invoke the AddMessage() method.
        /// </summary>
        protected delegate void AddMessageDelegate(
            string who, string privatelyTo, string message, Color messageColor);
        /// <summary>
        /// Adds chat message to the listview control, containing the messages.
        /// </summary>
        /// <param name="who">Who sent the message.</param>
        /// <param name="privatelyTo">
        /// If the message is private will contain the private receipee nickname;
        /// empty string - otherwise.
        /// </param>
        /// <param name="message">The text message itself.</param>
        /// <param name="messageColor">The forecolor of the message to display.</param>
        protected virtual void AddMessage(
            string who, string privatelyTo, string message, Color messageColor)
        {
            if (lstvMessages.InvokeRequired)
            {
                lstvMessages.BeginInvoke(
                    new AddMessageDelegate(AddMessage),
                    new object[] { who, privatelyTo, message, messageColor });
                return;
            }

            string privatly = string.IsNullOrEmpty(privatelyTo) ? 
                string.Empty : string.Format("(privately to {0})", privatelyTo);

            string header = string.Format(
                "On {0} {1} says{2}:", DateTime.Now, who, privatly);

            ListViewItem headerItem = new ListViewItem();
            headerItem.SubItems[0].Text = header;

            lstvMessages.Items.Add(headerItem);

            ListViewItem messageItem = new ListViewItem();
            messageItem.SubItems[0].Text = message.PadLeft(message.Length + 4);

            if (messageColor != Color.Empty)
                messageItem.SubItems[0].ForeColor = messageColor;

            lstvMessages.Items.Add(messageItem);

            //
            // ensure, the line just added to the listview is visible.
            messageItem.EnsureVisible();
        }
        /// <summary>
        /// Adds message, initiated by the chat server to the listview control,
        /// containing the messages.
        /// </summary>
        /// <param name="message">The message text.</param>
        private void AddServerMessage(string message)
        {
            AddMessage("Server", string.Empty, message, Color.Empty);
        }

        /// <summary>
        /// Delegate to invoke UpdateOnlineUsers() method.
        /// </summary>
        protected delegate void UpdateOnlineUsersDelegate(
            string[] onlineUsersArr, UserConnectState connectState);
        /// <summary>
        /// Updates the lbxOnlineUsers control, which displays the nicknames
        /// of currently logged in chat users, against the list of specified nicknames.
        /// These nicknames are either new logged in users or users that logged out.
        /// </summary>
        /// <param name="onlineUsersArr">
        /// The list of nicknames for update.
        /// </param>
        /// <param name="connectState">
        /// Whether the specified in the list users logged in or out.
        /// </param>
        protected virtual void UpdateOnlineUsers(
            string[] onlineUsersArr, UserConnectState connectState)
        {
            if (lbxOnlineUsers.InvokeRequired)
            {
                lbxOnlineUsers.Invoke(
                    new UpdateOnlineUsersDelegate(UpdateOnlineUsers),
                    new object[] { onlineUsersArr, connectState });
                return;
            }

            //
            // The event handler for the SelectedIndexChanged event of the
            // lbxOnlineUsers listbox control, checks the changed selected index
            // and if it is -1 (i.e. not selected), disables control(s), responsible
            // for sending private chat message. We just disable this event handler
            // for the time period when the lbxOnlineUsers listbox control items
            // get removed (RemoveOnlineUsers) or new items added to the control
            // in order to avoid unnecessary overhead.
            SetSelectedUserChangedEvent(false); // disable SelectedIndexChanged handler
            switch (connectState)
            {
                case UserConnectState.Connected:
                    AddOnlineUsers(onlineUsersArr);
                    break;
                case UserConnectState.Disconnected:
                    RemoveOnlineUsers(onlineUsersArr);
                    break;
                default:
                    break;
            }
            SetSelectedUserChangedEvent(true); // enable back the SelectedIndexChanged handler
        }

        /// <summary>
        /// Adds the specified users' nicknames to the lbxOnlineUsers control.
        /// </summary>
        /// <param name="onlineUsersArr">The nicknames to add</param>
        protected virtual void AddOnlineUsers(string[] onlineUsersArr)
        {
            lbxOnlineUsers.Items.AddRange(onlineUsersArr);
        }
        /// <summary>
        /// Removes the specified users' nicknames from the lbxOnlineUsers control.
        /// </summary>
        /// <param name="onlineUsers">
        /// The nicknames to remove. When null, all the items removed from the listbox.
        /// </param>
        protected virtual void RemoveOnlineUsers(string[] onlineUsers)
        {
            if (onlineUsers == null)
            {
                lbxOnlineUsers.Items.Clear();
            }
            else
            {
                foreach (string user in onlineUsers)
                {
                    lbxOnlineUsers.Items.Remove(user);
                }

                //
                // Here this function is called in order to disable the button
                // for sending private messages, in case the user selected in the 
                // lbxOnlineUsers listbox control removed.
                UpdateSendButtonLayout(true);
            }
        }

        /// <summary>
        /// Sends chat message to the specified recipient, taking the text of the
        /// message from the txtMessage textbox control.
        /// </summary>
        /// <param name="to">The message recipient.</param>
        protected virtual void SendMessage(NickName to)
        {
            string message = txtMessage.Text.Trim();
            if (message == string.Empty)
                return;

            txtMessage.Text = string.Empty;

            try
            {
                //
                // In case of the special text message, perform the stress test,
                // by sending the message in a long loop.
                if (message.StartsWith("@@SYSTEM_TEST_STRESS"))
                {
                    //
                    // Parse the remaining part of the message to get the number of
                    // times the message will be sent.
                    string[] paramtrs = message.Split(',');
                    int times = int.Parse(paramtrs[1]);
                    for (int i = 0; i < times; i++)
                    {
                        clientEndPoint.SendMessage(to, message);
                    }
                }
                else
                {
                    clientEndPoint.SendMessage(to, message);
                }
            }
            catch (Exception ex)
            {
                //
                // Treat any exception upon send as a connection problem, which
                // makes sense, assuming the bugless code....:-)
                MessageBox.Show(
                    "Oops, connection unexpectedly closed: " + ex.Message +
                    "\nPlease, reConnect");
                //
                // 
                // Sending is done in SendCommandMode.DisposeThrow, so no need to
                // dispose the clientEndPoint instanse.

                clientEndPoint = null;

                //
                // Switch the main form layout to 'disconnected'.
                UpdateConnectionStateLayout(false);
            }
        }

        protected virtual void ClientMainForm_Load(object sender, EventArgs e)
        {
            clientEndPoint = null;
            connectToDlg = new ConnectToDialog();

            //
            // Switch the main form layout to 'disconnected'.
            UpdateConnectionStateLayout(false);
        }

        /// <summary>
        /// Enables/Disables the event handler for the SelectedIndexChanged event
        /// of the lbxOnlineUsers control.
        /// </summary>
        /// <param name="set">true - enables the event handler; false - disables.</param>
        private void SetSelectedUserChangedEvent(bool set)
        {
            if (set)
            {
                lbxOnlineUsers.SelectedIndexChanged += new EventHandler(
                    lbxOnlineUsers_SelectedIndexChanged);
            }
            else
            {
                lbxOnlineUsers.SelectedIndexChanged -=
                    lbxOnlineUsers_SelectedIndexChanged;
            }
        }

        /// <summary>
        /// Creates the instanse of the clientEndPoint which will be used by the GUI.
        /// </summary>
        private void CreateEndPoint()
        {
            Connection<ClientCmd> conn = null;
            CmdSerializer<ClientCmd> ser = null;

            try
            {
                //
                // Create the connection with the chat server first, based on the
                // provided server's IP endpoint.
                KeyValuePair<Connection<ClientCmd>, CmdSerializer<ClientCmd>> connSer =
                    ConnectionFactory<ClientCmd>.Create(connectToDlg.IpEndPoint);

                conn = connSer.Key;
                ser = connSer.Value;
            }
            catch (Exception ex)
            {
                MessageBox.Show(string.Format("Oops, failed to connect: {0}", ex.Message));
                return;
            }

            try
            {
                //
                // Create the enpoint using the static wrapper. The ClientEndPoint ctor
                // will perform the login to the server.
                clientEndPoint = ClientEndPoint.Create(
                    connectToDlg.Login, conn, ser);
            }
            catch (Exception ex)
            {
                MessageBox.Show(string.Format("Oops, failed to login: {0}", ex.Message));

                //
                // No need to close the connection, the staic wrapper will do it by itself
                // upon failure.
                return;
            }

            //
            // Set the event handlers.
            //

            clientEndPoint.UsersOnline += new EventHandler<UsersOnlineEventArgs>(
                clientEndPoint_UsersOnline);
            clientEndPoint.UsersUpdate += new EventHandler<UsersUpdateEventArgs>(
                clientEndPoint_UsersUpdate);
            clientEndPoint.AbnormallyTerminated +=
                new EventHandler<EndPointAbnormallyTerminatedEventArgs>(
                    clientEndPoint_AbnormallyTerminated);
            clientEndPoint.MessageReceived += new EventHandler<MessageEventArgs>(
                clientEndPoint_MessageReceived);
        }

        /// <summary>
        /// Event handler for the SelectedIndexChanged of the lbxOnlineUsers control.
        /// </summary>
        private void lbxOnlineUsers_SelectedIndexChanged(object sender, EventArgs e)
        {
            //
            // Here this function is called in order to disable the button
            // for sending private messages, in case the selected index of the
            // lbxOnlineUsers listbox control became -1, i.e. no slection.
            UpdateSendButtonLayout(true);
        }

        /// <summary>
        /// Connect to the chat server.
        /// </summary>
        private void connectToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Connect();
        }
        /// <summary>
        /// Disconnect from the chat server.
        /// </summary>
        private void disconnectToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Disconnect();
        }

        /// <summary>
        /// Event handler for the UsersOnline event risen by the clientEndPoint instanse.
        /// </summary>
        private void clientEndPoint_UsersOnline(object sender, UsersOnlineEventArgs e)
        {
            string onlineUsersCsv = e.CommandReceived.OnlineUsersCsv;

            if (onlineUsersCsv == string.Empty)
                return;

            string[] onlineUsersArr = onlineUsersCsv.Split(new char[] { ',' });

            //
            // Update control(s) displaying online users.
            UpdateOnlineUsers(onlineUsersArr, UserConnectState.Connected);
        }
        /// <summary>
        /// Event handler for the UsersUpdate event risen by the clientEndPoint instanse.
        /// </summary>
        private void clientEndPoint_UsersUpdate(object sender, UsersUpdateEventArgs e)
        {
            string usersCsv = e.CommandReceived.UsersCsv;

            string[] usersArr = usersCsv.Split(new char[] { ',' });

            //
            // Update control(s) displaying online users.
            UpdateOnlineUsers(usersArr, e.CommandReceived.ConnectState);

            string what = e.CommandReceived.ConnectState == UserConnectState.Connected ?
                "connected" : "disconnected";

            //
            // For each user disconnected/connected to the chat, display the notification
            // in the control that displays the chat messages.
            foreach (string user in usersArr)
            {
                string message = string.Format("{0} {1}", user, what);
                AddServerMessage(message);
            }
        }
        /// <summary>
        /// Event handler for the MessageReceived event risen by the clientEndPoint instanse.
        /// </summary>
        private void clientEndPoint_MessageReceived(object sender, MessageEventArgs e)
        {
            string privatelyTo = e.CommandReceived.MessageData.To == NickName.Broadcast ?
                string.Empty : e.CommandReceived.MessageData.To.Value;

            //
            // Display the received message in the control that displays the chat messages.
            AddMessage(
                e.CommandReceived.MessageData.Sender.Value,
                privatelyTo,
                e.CommandReceived.MessageData.Message,
                e.CommandReceived.TextColor);
        }
        /// <summary>
        /// Event handler for the AbnormallyTerminated event risen by the clientEndPoint instanse. 
        /// </summary>
        private void clientEndPoint_AbnormallyTerminated(object sender, EndPointAbnormallyTerminatedEventArgs e)
        {
            //
            // The clientEndPoint instanse will dispose itself before rising this event.
            clientEndPoint = null;

            MessageBox.Show(string.Format(
                "Oops, connection to server unexpectedly lost: {0}\nPlease, reConnect",
                e.ReasonException.Message));

            //
            // Switch the main form layout to 'disconnected'.
            UpdateConnectionStateLayout(false);
        }

        /// <summary>
        /// Event handler for the Click event of the Send button.
        /// </summary>
        private void btnSend_Click(object sender, EventArgs e)
        {
            SendMessage(NickName.Broadcast);
        }
        /// <summary>
        /// Event handler for the Click event of the Send Private button.
        /// </summary>
        private void btnSendPrivate_Click(object sender, EventArgs e)
        {
            NickName to = new NickName(
                lbxOnlineUsers.Items[lbxOnlineUsers.SelectedIndex].ToString());

            SendMessage(to);
        }

        /// <summary>
        /// Updtates layout of all controls of the main form which are subject to change
        /// upon the client connection state change (connected/disconnected)
        /// </summary>
        /// <param name="connected">Whether the server started or stopped.</param>
        private void UpdateConnectionStateLayout(bool connected)
        {
            // update the toolstrip status label.
            UpdateConnectionStateLabel(connected);
            // update the main menu.
            UpdateMainMenuLayout(connected);

            // Clear control(s) displaying online users if disconnected.
            if (!connected)
                ClearOnlineUsers();

            //
            // Update Send and Send Private buttons.
            UpdateSendButtonLayout(connected);
            //
            // Change the main form title etc.
            UpdateMainWindowLayout(connected);
        }

        /// <summary>
        /// Clears the lbxOnlineUsers listbox control which displays the online chat users.
        /// </summary>
        private void ClearOnlineUsers()
        {
            UpdateOnlineUsers(null, UserConnectState.Disconnected);
        }

        /// <summary>
        /// Event handler for the FormClosed event of the main form.
        /// Will dispose the clientEndPoint instanse.
        /// </summary>
        private void ClientMainForm_FormClosed(object sender, FormClosedEventArgs e)
        {
            if (clientEndPoint != null)
            {
                clientEndPoint.Dispose();
                clientEndPoint = null;
            }

        }

        /// <summary>
        /// Will send broadcast message if just 'Enter' or private message
        /// if ctrl+Enter.
        /// </summary>
        private void txtMessage_KeyDown(object sender, KeyEventArgs e)
        {
            switch (e.KeyCode)
            {
                case Keys.Enter:
                    if (e.Control)
                    {
                        if (lbxOnlineUsers.SelectedIndex >= 0)
                        {
                            NickName to = new NickName(
                                lbxOnlineUsers.Items[lbxOnlineUsers.SelectedIndex].ToString());

                            SendMessage(to);
                        }
                    }
                    else
                    {
                        SendMessage(NickName.Broadcast);
                    }
                    break;

                default:
                    break;
            }
        }
    }
}
